<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>pixiJs</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.5.5/pixi.min.js"></script>
    <script src="./scaleToWindow.js"></script>
    <script src="https://www.kkkk1000.com/js/spriteUtilities.js"></script>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      body {
        background-color: #e3e3e3;
      }
    </style>
  </head>

  <body>
    <script type="text/javascript">
      // PIXE默认使用WebGL渲染
      let type = 'WebGL'
      // 通过PIXI自带的方法判断当前浏览器是否支持WebGL渲染
      if (!PIXI.utils.isWebGLSupported()) {
        // 如果不支持WebGL渲染，则使用canvas渲染
        type = 'canvas'
      }
      // 指定PIXI的渲染引擎
      PIXI.utils.sayHello(type)

      let su = new SpriteUtilities(PIXI)

      // 使用别名
      let Application = PIXI.Application,
        loader = PIXI.loader,
        resources = PIXI.loader.resources,
        Sprite = PIXI.Sprite,
        TextureCache = PIXI.utils.TextureCache,
        Rectangle = PIXI.Rectangle,
        Container = PIXI.Container,
        Graphics = PIXI.Graphics,
        Text = PIXI.Text,
        TextStyle = PIXI.TextStyle,
        AnimatedSprite = PIXI.extras.AnimatedSprite,
        Texture = PIXI.Texture

      // 创建一个 Pixi 应用程序
      let app = new Application({
        width: 512, // default: 800 宽度
        height: 512, // default: 600 高度
        antialias: true, // default: false 反锯齿
        transparent: false, // default: false 透明度
        resolution: 1 // default: 1 分辨率
        // 是否强制使用canvas渲染(因为WebGL非常快，所以Pixi的renderer对象将默认为WebGL)
        // forceCanvas: true,
        // backgroundColor: '0x66FF33' // 舞台背景颜色
      })
      // 通过插件使画布按比例缩放到任何浏览器窗口大小
      scaleToWindow(app.view, '#e3e3e3')

      // 将Pixi自动为您创建的画布添加到HTML文档中
      document.body.appendChild(app.view)

      // 使用Pixi强大的loader（加载器）对象加载图像,加载完成时调用一个名为onImageLoadComplete的函数
      loader
        .add(['images/treasureHunter.json', 'images/blob-1.gif', 'images/Pikachu.png', 'images/gengar.png', 'images/enter_start.png', 'images/start_btn.png', 'images/reset_game_btn.png'])
        .load(onImageLoadComplete)

      // 声明变量
      let dungeon, explorer, treasure, id, door, state, healthBar, gameScene, gameOverScene, explorerHit, blobs, startBtn

      // 回调函数,当加载程序加载完图像后触发
      function onImageLoadComplete() {
        // 创建主游戏场景的分组容器(所有属于主游戏的精灵都被添加到gameScene分组)
        gameScene = new Container()
        // 创建游戏结束的分组容器(游戏结束时显示的文本将被添加到gameOverScene分组中)
        gameOverScene = new Container()
        // 创建生命条的分组容器
        healthBar = new PIXI.Container()

        // 起一个别名
        id = resources['images/treasureHunter.json'].textures
        // 加载背景图片
        dungeonTexture = TextureCache['dungeon.png'] // 背景墙
        // 根据背景图片创建精灵
        dungeon = new Sprite(dungeonTexture)
        // 将精灵添加到舞台中
        app.stage.addChild(dungeon)

        // 调用 su 对象的 filmstrip 方法将雪碧图转换为可用于制作精灵的纹理数组
        let pikachuList = su.filmstrip('images/Pikachu.png', 48, 48)
        // 通过纹理贴图集创建动画精灵
        explorer = su.sprite(pikachuList) // 人物
        // 设置动画精灵的速度
        explorer.fps = 7
        // 将每个键的值设置为与状态对应的帧编号
        explorer.states = {
          down: 0,
          left: 4,
          right: 8,
          up: 12,
          walkDown: [0, 3],
          walkLeft: [4, 7],
          walkRight: [8, 11],
          walkUp: [12, 15]
        }
        // 设置精灵的位置
        explorer.x = 68
        // 使人物图片位置垂直居中
        explorer.y = app.stage.height / 2 - explorer.height / 2
        // 设置精灵的速度变量
        explorer.vx = 0
        explorer.vy = 0
        // 将精灵添加到主游戏场景容器中
        gameScene.addChild(explorer)
        // 显示精灵的某一静止状态
        explorer.show(explorer.states.down)
        // 播放精灵动画
        // explorer.playAnimation(explorer.states.walkDown)

        // 通过纹理贴图集创建精灵
        treasure = new Sprite(id['treasure.png']) // 宝箱
        // 设置精灵的位置
        treasure.x = app.stage.width - treasure.width - 48
        treasure.y = app.stage.height / 2 - treasure.height / 2
        // 将精灵添加到主游戏场景中
        gameScene.addChild(treasure)

        // 通过纹理贴图集创建精灵
        door = new Sprite(id['door.png']) // 出口大门
        // 设置精灵的位置
        door.position.set(32, 0)
        // 将精灵添加到主游戏场景中
        gameScene.addChild(door)

        // 开始制作斑点
        let numberOfBlobs = 8, // 斑点数量
          // 斑点间隔
          spacing = 36,
          // 第一个斑点的x轴偏移量
          xOffset = 150,
          // 斑点移动速度
          speed = 1,
          // 斑点移动方向(向上还是向下)
          direction = 1
        // 存储所有斑点的阵列
        blobs = []
        // 制作对应数量的斑点
        for (let i = 0; i < numberOfBlobs; i++) {
          // 调用 su 对象的 filmstrip 方法将雪碧图转换为可用于制作精灵的纹理数组
          let blobList = su.filmstrip('images/gengar.png', 48, 48)
          // 通过纹理贴图集创建动画精灵
          let blob = su.sprite(blobList) // 斑点
          // 将每个键的值设置为与状态对应的帧编号
          blob.states = {
            down: 0,
            left: 4,
            right: 8,
            up: 12,
            walkDown: [0, 3],
            walkLeft: [4, 7],
            walkRight: [8, 11],
            walkUp: [12, 15]
          }
          // 设置动画精灵的速度
          blob.fps = 7
          // 将每个键的值设置为与状态对应的帧编号
          blob.states = {
            down: 0,
            left: 4,
            right: 8,
            up: 12,
            walkDown: [0, 3],
            walkLeft: [4, 7],
            walkRight: [8, 11],
            walkUp: [12, 15]
          }
          // 根据“间距”值水平分隔每个水滴
          let x = spacing * i + xOffset
          // 调用获取随机数的方法让斑点的y轴偏移量随机
          let y = randomInt(0, app.stage.height - blob.height)
          // 设置精灵的位置
          blob.x = x
          blob.y = y
          // 设置斑点的速度和正反方向方向(速度 * 正反方向)
          blob.vy = speed * direction
          // 反转下一个斑点的方向
          direction *= -1
          // 将当前斑点添加到斑点阵列
          blobs.push(blob)
          // 将当前斑点添加到主游戏场景容器中
          gameScene.addChild(blob)
          // 根据正反方向展示不同的动画
          if (direction === 1) {
            // 精灵方向
            blob.show(blob.states.up)
          } else {
            // 精灵方向
            blob.show(blob.states.down)
          }
        }

        // 设置生命条分组容器的位置
        healthBar.position.set(app.stage.width - 170, 4)
        // 创建一个图形实例(创建一个黑色背景的矩形)
        let innerBar = new Graphics()
        // 填充颜色
        innerBar.beginFill(0x000000)
        // 绘制形状
        innerBar.drawRect(0, 0, 128, 8)
        // 结束绘制
        innerBar.endFill()
        // 添加到生命条分组容器中
        healthBar.addChild(innerBar)
        // 创建一个图形实例(创建一个红色背景的矩形)
        let outerBar = new Graphics()
        // 填充颜色
        outerBar.beginFill(0xff3300)
        // 绘制形状
        outerBar.drawRect(0, 0, 128, 8)
        // 结束绘制
        outerBar.endFill()
        // 添加到生命条分组容器中
        healthBar.addChild(outerBar)
        // 将红色矩形实例挂载到生命条容器上(方便以后调用)
        healthBar.outer = outerBar
        // 将生命条分组容器添加到主游戏场景容器中
        gameScene.addChild(healthBar)

        // 开始按钮
        startBtnTextureCache = TextureCache['images/start_btn.png']
        // 创建精灵
        startBtn = new Sprite(startBtnTextureCache)
        // 设置精灵位置
        startBtn.x = app.stage.width / 2 - startBtn.width / 2
        startBtn.y = app.stage.height / 2 - startBtn.height / 2
        // 将精灵添加到舞台中
        gameScene.addChild(startBtn)

        // 重新开始按钮
        resetGameBtnTextureCache = TextureCache['images/reset_game_btn.png']
        // 创建精灵
        resetGameBtn = new Sprite(resetGameBtnTextureCache)
        // 设置精灵位置
        resetGameBtn.x = app.stage.width / 2 - resetGameBtn.width / 2
        resetGameBtn.y = app.stage.height / 2 - resetGameBtn.height / 2
        // 将精灵添加到舞台中
        gameScene.addChild(resetGameBtn)
        // 隐藏重新开始按钮
        resetGameBtn.visible = false

        // 设置游戏消息文本的样式
        let style = new TextStyle({
          fontFamily: 'Futura',
          fontSize: 64,
          fill: 'white'
        })
        // 创建游戏文本
        message = new Text('The End!', style)
        // 设置游戏文本的位置
        message.x = 120
        message.y = app.stage.height / 2 - 32
        // 将游戏文本添加到游戏结束场景中
        gameOverScene.addChild(message)

        // 捕获键盘上的上下左右四个箭头键
        let left = keyboard('ArrowLeft'),
          up = keyboard('ArrowUp'),
          right = keyboard('ArrowRight'),
          down = keyboard('ArrowDown'),
          enter = keyboard('Enter')

        // 处理函数,当enter键被点击时触发
        enter.press = () => {
          // 处理函数,当左箭头键被按下时触发
          left.press = () => {
            // 改变人物方向
            explorer.show(explorer.states.left)
            // 播放人物精灵动画
            explorer.playAnimation(explorer.states.walkLeft)
            // 改变人物的速度
            explorer.vx = -3
            explorer.vy = 0
          }
          // 处理函数,当左箭头键被释放时触发
          left.release = () => {
            // 如果左箭头键释放的时候,其它三个方向键都没有按下
            if (!right.isDown && explorer.vy === 0) {
              // 停止精灵移动
              explorer.vx = 0
              // 显示精灵某一静止状态
              explorer.show(explorer.states.left)
            }
          }
          // 处理函数,当上箭头键被按下时触发
          up.press = () => {
            // 改变人物方向
            explorer.show(explorer.states.up)
            // 播放人物精灵动画
            explorer.playAnimation(explorer.states.walkUp)
            // 改变人物的速度
            explorer.vy = -3
            explorer.vx = 0
          }
          // 处理函数,当上箭头键被释放时触发
          up.release = () => {
            // 如果上箭头键释放的时候,其它三个方向键都没有按下
            if (!down.isDown && explorer.vx === 0) {
              // 停止精灵移动
              explorer.vy = 0
              // 显示精灵某一静止状态
              explorer.show(explorer.states.up)
            }
          }
          // 处理函数,当右箭头键被按下时触发
          right.press = () => {
            // 改变人物方向
            explorer.show(explorer.states.right)
            // 播放人物精灵动画
            explorer.playAnimation(explorer.states.walkRight)
            // 改变人物的速度
            explorer.vx = 3
            explorer.vy = 0
          }
          // 处理函数,当右箭头键被释放时触发
          right.release = () => {
            // 如果右箭头键释放的时候,其它三个方向键都没有按下
            if (!left.isDown && explorer.vy === 0) {
              // 停止精灵移动
              explorer.vx = 0
              // 显示精灵某一静止状态
              explorer.show(explorer.states.right)
            }
          }
          // 处理函数,当下箭头键被按下时触发
          down.press = () => {
            // 改变人物方向
            explorer.show(explorer.states.down)
            // 播放人物精灵动画
            explorer.playAnimation(explorer.states.walkDown)
            // 改变人物的速度
            explorer.vy = 3
            explorer.vx = 0
          }
          down.release = () => {
            // 如果下箭头键释放的时候,其它三个方向键都没有按下
            if (!up.isDown && explorer.vx === 0) {
              // 显示精灵某一静止状态
              explorer.show(explorer.states.down)
              // 停止精灵移动
              explorer.vy = 0
            }
          }
          // 开启斑点的动画
          blobs.forEach((blob) => {
            // 根据正反方向展示不同的动画
            if (blob.vy > 0) {
              // 播放精灵动画
              blob.playAnimation(blob.states.walkDown)
            } else {
              // 播放精灵动画
              blob.playAnimation(blob.states.walkUp)
            }
          })
          // 隐藏开始按钮
          startBtn.visible = false
          // 开始游戏循环
          app.ticker.add((delta) => gameLoop(delta))
        }
        // 处理函数,当enter键释放时触发
        enter.release = () => {
          // 使用 unsubscribe 方法删除事件监听器
          enter.unsubscribe()
        }

        // 将主游戏场景容器添加到舞台中
        app.stage.addChild(gameScene)
        // 将游戏结束场景容器添加到舞台中
        app.stage.addChild(gameOverScene)
        // 隐藏结束游戏时的场景容器(只有游戏结束时才显示)
        gameOverScene.visible = false

        // 设置这个游戏的状态
        state = play
        // 开始游戏循环
        // app.ticker.add((delta) => gameLoop(delta))
      }

      // 在取值范围内随机取值的方法(获取min到max之间的随机数)
      function randomInt(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min
      }

      // 处理函数,当开始游戏循环时触发
      function gameLoop(delta) {
        // 更新当前游戏的状态
        state(delta)
      }

      // 游戏运行状态
      function play(delta) {
        // 移动探险者并将其包含在地牢中
        explorer.x += explorer.vx
        explorer.y += explorer.vy
        // 移动斑点怪物
        blobs.forEach(function (blob) {
          // 移动斑点
          blob.y += blob.vy
          // 检查斑点是否触碰到边界
          let blobHitsWall = contain(blob, { x: 28, y: 10, width: 488, height: 480 })
          // 如果斑点触碰到顶部或者底部边界
          if (blobHitsWall === 'top' || blobHitsWall === 'bottom') {
            // 反转它的移动方向
            blob.vy *= -1
            // 如果是触碰底部边界
            if (blob.vy < 0) {
              // 改变斑点精灵方向
              blob.show(blob.states.up)
              // 播放斑点精灵动画
              blob.playAnimation(blob.states.walkUp)
            } else {
              // 改变斑点精灵方向
              blob.show(blob.states.down)
              // 播放斑点精灵动画
              blob.playAnimation(blob.states.walkDown)
            }
          }

          // 测试人物与斑点的碰撞,如果两者有碰撞的话
          if (hitTestRectangle(explorer, blob)) {
            // 更新人物生命状态
            explorerHit = true
          }

          // 检查斑点和资源管理器之间是否存在冲突
          if (explorerHit) {
            // 如果人物与斑点有碰撞,则让人物变成半透明
            explorer.alpha = 0.5
            // 更新生命条(减小红色矩形的宽度)
            healthBar.outer.width -= 1
          } else {
            // 如果未和斑点碰撞,则让人物不透明
            explorer.alpha = 1
          }
        })
        // 检查人物是否有触碰游戏边界
        let borderHitsWall = contain(explorer, { x: 28, y: 10, width: 488, height: 480 })
        // 如果人物有碰撞边界
        if (borderHitsWall === 'top' || borderHitsWall === 'bottom' || borderHitsWall === 'right' || borderHitsWall === 'left') {
          // 更新人物生命状态
          explorerHit = true
        }
        // 检查探险者和宝藏之间的碰撞
        if (hitTestRectangle(explorer, treasure)) {
          // 更新宝箱的位置,使其与人物同步
          treasure.x = explorer.x + 8
          treasure.y = explorer.y + 8
        }
        // 检查宝藏和门之间有无碰撞
        if (hitTestRectangle(treasure, door)) {
          // 如果宝箱与出口大门有碰撞,则更新游戏状态
          state = end
          // 更新游戏结束提示信息
          message.text = '你成功了!'
        }
        // 决定这场比赛是赢了还是输了
        if (healthBar.outer.width < 0) {
          // 如果生命条归零,则更新游戏状态
          state = end
          // 更新游戏结束提示信息
          message.text = '你失败了!'
        }
        // 游戏结束后，将游戏“状态”更改为“结束”
      }

      // 游戏结束状态
      function end() {
        // 隐藏主游戏场景容器
        gameScene.visible = false
        // 显示游戏结束场景容器
        gameOverScene.visible = true
      }

      // 监听并捕获键盘事件的方法
      function keyboard(value) {
        let key = {}
        key.value = value
        key.isDown = false
        key.isUp = true
        key.press = undefined
        key.release = undefined
        //The `downHandler`
        key.downHandler = (event) => {
          if (event.key === key.value) {
            if (key.isUp && key.press) key.press()
            key.isDown = true
            key.isUp = false
            event.preventDefault()
          }
        }

        //The `upHandler`
        key.upHandler = (event) => {
          if (event.key === key.value) {
            if (key.isDown && key.release) key.release()
            key.isDown = false
            key.isUp = true
            event.preventDefault()
          }
        }

        //Attach event listeners
        const downListener = key.downHandler.bind(key)
        const upListener = key.upHandler.bind(key)

        window.addEventListener('keydown', downListener, false)
        window.addEventListener('keyup', upListener, false)

        // Detach event listeners
        key.unsubscribe = () => {
          window.removeEventListener('keydown', downListener)
          window.removeEventListener('keyup', upListener)
        }

        return key
      }

      // 检查是否触碰到游戏边界的方法
      function contain(sprite, container) {
        let collision = undefined

        //Left
        if (sprite.x < container.x) {
          sprite.x = container.x
          collision = 'left'
        }

        //Top
        if (sprite.y < container.y) {
          sprite.y = container.y
          collision = 'top'
        }

        //Right
        if (sprite.x + sprite.width > container.width) {
          sprite.x = container.width - sprite.width
          collision = 'right'
        }

        //Bottom
        if (sprite.y + sprite.height > container.height) {
          sprite.y = container.height - sprite.height
          collision = 'bottom'
        }

        //Return the `collision` value
        return collision
      }

      // 检测两个精灵之间是否有碰撞
      function hitTestRectangle(r1, r2) {
        //Define the variables we'll need to calculate
        let hit, combinedHalfWidths, combinedHalfHeights, vx, vy

        //hit will determine whether there's a collision
        hit = false

        //Find the center points of each sprite
        r1.centerX = r1.x + r1.width / 2
        r1.centerY = r1.y + r1.height / 2
        r2.centerX = r2.x + r2.width / 2
        r2.centerY = r2.y + r2.height / 2

        //Find the half-widths and half-heights of each sprite
        r1.halfWidth = r1.width / 2
        r1.halfHeight = r1.height / 2
        r2.halfWidth = r2.width / 2
        r2.halfHeight = r2.height / 2

        //Calculate the distance vector between the sprites
        vx = r1.centerX - r2.centerX
        vy = r1.centerY - r2.centerY

        //Figure out the combined half-widths and half-heights
        combinedHalfWidths = r1.halfWidth + r2.halfWidth
        combinedHalfHeights = r1.halfHeight + r2.halfHeight

        //Check for a collision on the x axis
        if (Math.abs(vx) < combinedHalfWidths) {
          //A collision might be occurring. Check for a collision on the y axis
          if (Math.abs(vy) < combinedHalfHeights) {
            //There's definitely a collision happening
            hit = true
          } else {
            //There's no collision on the y axis
            hit = false
          }
        } else {
          //There's no collision on the x axis
          hit = false
        }

        //`hit` will be either `true` or `false`
        return hit
      }
    </script>
  </body>
</html>
